package eo.application.hibernate;

import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import javax.persistence.EntityManagerFactory;
import javax.persistence.spi.PersistenceUnitTransactionType;

import org.hibernate.EntityMode;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.cfg.Environment;
import org.hibernate.ejb.EntityManagerFactoryImpl;
import org.hibernate.metadata.ClassMetadata;

import eo.application.hibernate.support.TopLevelDialogSessionContext;
import eo.builder.property.HibernateIdentityPropertyDescriptor;
import eo.builder.property.HibernateMetaClassPropertyDescriptor;
import eo.descriptor.configuration.ApplicationInstance;
import eo.descriptor.entity.EntityDescriptor;
import eo.descriptor.entity.impl.SimpleEntityDescriptor;
import eo.descriptor.property.PropertyDescriptor;
import eo.descriptor.query.NamedQueryDescriptor;

public class HibernateApplicationInstance implements ApplicationInstance {

    private final SessionFactory sessionFactory;

    private final Map<String, String> connectionProperties,
            configurationProperties;

    private final String connectionURL;

    private final Map<String, NamedQueryDescriptor> declaredQueries;

    private final Map<String, EntityDescriptor> descriptors = new HashMap<String, EntityDescriptor>();

    private final EntityManagerFactory entityManagerFactory;

    public HibernateApplicationInstance(Map<String, String> connectionProps,
            Map<String, String> configProps, String connUrl,
            Map<String, NamedQueryDescriptor> queries, String dialect,
            String driverClassName, final URL... configUrls) {
        super();
        AnnotationConfiguration cfg = new AnnotationConfiguration();
        for (URL url : configUrls) {
            cfg.configure(url);
        }
        Set<Entry<String, String>> entrySet = connectionProps.entrySet();
        for (Entry<String, String> entry : entrySet) {
            cfg.setProperty(entry.getKey(), entry.getValue());
        }
        Set<Entry<String, String>> entries = configProps.entrySet();
        for (Entry<String, String> entry : entries) {
            cfg.setProperty(entry.getKey(), entry.getValue());
        }
        cfg.setProperty(Environment.CURRENT_SESSION_CONTEXT_CLASS,
                TopLevelDialogSessionContext.class.getName());
        cfg.setProperty(Environment.DIALECT, dialect);
        this.sessionFactory = cfg.buildSessionFactory();
        this.configurationProperties = Collections
                .unmodifiableMap(new HashMap<String, String>(configProps));
        this.connectionProperties = Collections
                .unmodifiableMap(new HashMap<String, String>(connectionProps));
        this.connectionURL = connUrl;
        this.declaredQueries = Collections
                .unmodifiableMap(new HashMap<String, NamedQueryDescriptor>(
                        queries));
        this.parseEntities();
        this.entityManagerFactory = new EntityManagerFactoryImpl(
                this.sessionFactory,
                PersistenceUnitTransactionType.RESOURCE_LOCAL, true);
    }

    @SuppressWarnings("unchecked")
    private void parseEntities() {
        Map<String, ClassMetadata> metadata = this.sessionFactory
                .getAllClassMetadata();
        Map<Class<?>, SimpleEntityDescriptor> mapped = new HashMap<Class<?>, SimpleEntityDescriptor>();
        for (ClassMetadata meta : metadata.values()) {
            SimpleEntityDescriptor desc = this
                    .createDescriptorFromMetadata(meta);
            mapped.put(desc.getEntityClass(), desc);
        }
        Set<Entry<Class<?>, SimpleEntityDescriptor>> entrySet = mapped
                .entrySet();
        for (Entry<Class<?>, SimpleEntityDescriptor> entry : entrySet) {
            Class<?> superType = entry.getKey().getSuperclass();
            if (superType != null && superType != Object.class) {
                SimpleEntityDescriptor superDescriptor = mapped.get(superType);
                if (superDescriptor != null) {
                    superDescriptor.addSubclass(entry.getValue());
                    entry.getValue().setSuperClassDescriptor(superDescriptor);
                }
            }
            this.descriptors.put(entry.getValue().getEntityName(), entry
                    .getValue());
        }
    }

    private SimpleEntityDescriptor createDescriptorFromMetadata(
            ClassMetadata meta) {
        Class<?> mappedClass = meta.getMappedClass(EntityMode.POJO);
        SimpleEntityDescriptor entity = new SimpleEntityDescriptor(meta
                .getEntityName(), mappedClass);
        PropertyDescriptor idDesc = this.createIdProperty(meta, mappedClass);
        entity.setIdDescriptor(idDesc);
        String[] propertyNames = meta.getPropertyNames();
        for (int i = 0; i < propertyNames.length; i++) {
            String string = propertyNames[i];
            PropertyDescriptor desc = createPropertDescriptor(meta, string,
                    mappedClass);
            entity.addProperty(desc);
        }
        return entity;
    }

    private PropertyDescriptor createPropertDescriptor(ClassMetadata meta,
            String string, Class<?> mappedClass) {
        return new HibernateMetaClassPropertyDescriptor(mappedClass, string,
                meta);
    }

    private PropertyDescriptor createIdProperty(ClassMetadata meta,
            Class<?> entityClass) {
        String idPropertyName = meta.getIdentifierPropertyName();
        return new HibernateIdentityPropertyDescriptor(entityClass,
                idPropertyName, meta);
    }

    /**
     * @return the sessionFactory
     */
    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    /**
     * @return the connectionProperties
     */
    public Map<String, String> getConnectionProperties() {
        return connectionProperties;
    }

    /**
     * @return the configurationProperties
     */
    public Map<String, String> getConfigurationProperties() {
        return configurationProperties;
    }

    /**
     * @return the declaredQueries
     */
    public Map<String, NamedQueryDescriptor> getDeclaredQueries() {
        return declaredQueries;
    }

    public Map<String, EntityDescriptor> getEntitiesDescriptors() {
        return this.descriptors;
    }

    /**
     * @return the connectionUrl
     */
    public String getConnectionURL() {
        return this.connectionURL;
    }

    public void close() {
        this.sessionFactory.close();
    }

    public EntityManagerFactory getEntityManagerFactory() {
        return this.entityManagerFactory;
    }

}
